﻿using System;
using System.Collections.Generic;
using System.IO;
using System.IO.IsolatedStorage;
using System.Linq;
using System.Threading.Tasks;

namespace FreshMeat.Storage
{
   /// <summary>
   /// Windows file system implementation of <see cref="IFileSystem"/>.
   /// </summary>
   public class IsolatedStorageFileSystem : IFileSystem
   {
      readonly IsolatedStorageFile area;
      readonly string root;
      readonly Task nothing = TaskEx.FromResult<object>(null);

      /// <summary>
      /// Creates instance.
      /// </summary>
      /// <param name="area">The isolated storage area to use.</param>
      /// <param name="root">Path within the area to use as a root or either null or an empty string to use entire area.</param>
      public IsolatedStorageFileSystem(IsolatedStorageFile area, string root = null)
      {
         if (area == null)
            throw new ArgumentNullException("area");

         this.area = area;
         this.root = root ?? "";

#if WP71
         UseEmulatedSearch = true; // WP71 has a known defect which prevents it from searching for anything but "*"
#else
         UseEmulatedSearch = false;
#endif
      }

      /// <summary>
      /// The isolated storage area.
      /// </summary>
      public IsolatedStorageFile Area { get { return area; } }
      
      /// <summary>
      /// The root folder or "" when using the entire area.
      /// </summary>
      public string Root { get { return root; } }

      /// <summary>
      /// Whether to use <see cref="WildcardSearchEmulator"/> to emulate wildcard search rules.
      /// Defaults to false except for Windows Phone 7.1 where it defaults to true due to
      /// a known defect that prevents it from searching for anything other than "*".
      /// </summary>
      public bool UseEmulatedSearch { get; set; }

      /// <summary>
      /// Creates a folder.
      /// If the folder already exists, this does nothing.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      public void CreateFolder(string path)
      {
         var combined = Path.Append(root, path);
         area.CreateDirectory(combined);
      }

      /// <summary>
      /// Creates a folder.
      /// If the folder already exists, this does nothing.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      public Task CreateFolderAsync(string path)
      {
         CreateFolder(path);
         return nothing;
      }

      /// <summary>
      /// Deletes a folder.
      /// If the folder does not exist, this does nothing.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      /// <returns>True if the file was deleted; false if the file did not exist.</returns>
      public bool DeleteFolder(string path)
      {
         var combined = Path.Append(root, path);
         var existed = area.DirectoryExists(combined);
         if (existed)
            area.DeleteDirectory(combined);
         return existed;
      }

      /// <summary>
      /// Deletes a folder.
      /// If the folder does not exist, this does nothing.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      public Task<bool> DeleteFolderAsync(string path)
      {
         var existed = DeleteFolder(path);
         return TaskEx.FromResult(existed);
      }

      /// <summary>
      /// Determines whether a folder exists.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      public bool FolderExists(string path)
      {
         var combined = Path.Append(root, path);
         return area.DirectoryExists(combined);
      }

      /// <summary>
      /// Determines whether a folder exists.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      public Task<bool> FolderExistsAsync(string path)
      {
         var exists = FolderExists(path);
         return TaskEx.FromResult(exists);
      }

      /// <summary>
      /// Enumerates the files in a folder.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      /// <param name="searchPattern">The search pattern, e.g. "*.txt" or "Doc?.txt.</param>
      /// <param name="recurse">True to recurse into subfolders, false to only check the specified folder.</param>
      /// <returns>Paths to files which match the search pattern.</returns>
      public IEnumerable<string> EnumerateFiles(string path, string searchPattern, bool recurse)
      {
         // In order for this to work across platforms and/or with partial trust it is necessary
         // to use IsolatedStorageFile.GetFileNames as the workhorse. More powerful methods
         // such as DirectoryInfo.GetFiles are security-restricted and not supported everywhere.

         var results = new List<string>();

         EnumerateFiles(
            path,
            UseEmulatedSearch ? "*" : searchPattern,
            UseEmulatedSearch ? new WildcardSearchEmulator(searchPattern) : null,
            recurse, 
            results);

         return results;
      }

      void EnumerateFiles(string path, string searchPattern, WildcardSearchEmulator emulator, bool recurse, List<string> accumulatedFiles)
      {
         var folderPath = Path.Append(root, path);
         var wildcardPath = Path.Append(folderPath, searchPattern);

         var files = area.GetFileNames(wildcardPath);
         if (emulator != null)
            files = files.Where(emulator.IsMatch).ToArray();

         var paths = files.Select(file => Path.Append(path, file)).ToArray();

         accumulatedFiles.AddRange(paths);

         if (!recurse) 
            return;

         foreach (var folderName in area.GetDirectoryNames(folderPath + "\\*"))
         {
            var subPath = Path.Append(path, folderName);
            EnumerateFiles(subPath, searchPattern, emulator, true, accumulatedFiles);
         }
      }

      /// <summary>
      /// Enumerates the files in a folder.
      /// </summary>
      /// <param name="path">The path to the folder.</param>
      /// <param name="searchPattern">The search pattern, e.g. "*.txt" or "Doc?.txt.</param>
      /// <param name="recurse">True to recurse into subfolders, false to only check the specified folder.</param>
      /// <returns>Paths to files which match the search pattern.</returns>
      public Task<IEnumerable<string>> EnumerateFilesAsync(string path, string searchPattern, bool recurse)
      {
         return TaskEx.Run(() => EnumerateFiles(path, searchPattern, recurse));
      }

      /// <summary>
      /// Moves and/or renames a file.
      /// An exception will be thrown if the destination file already exists.
      /// </summary>
      /// <param name="sourcePath">The path to the source file.</param>
      /// <param name="destinationPath">The path to the destination file.</param>
      public void MoveFile(string sourcePath, string destinationPath)
      {
         var fullSourcePath = Path.Append(root, sourcePath);
         var fullDestinationPath = Path.Append(root, destinationPath);
         try
         {
            area.MoveFile(fullSourcePath, fullDestinationPath);
         }
         catch (IOException) // source doesn't exist
         {
            throw new InvalidOperationException("Cannot move file because source doesn't exist.");
         }
         catch (IsolatedStorageException) // destination already exists
         {
            throw new InvalidOperationException("Cannot move file because destination already exists.");
         }
      }

      /// <summary>
      /// Moves and/or renames a file.
      /// An exception will be thrown if the destination file already exists.
      /// </summary>
      /// <param name="sourcePath">The path to the source file.</param>
      /// <param name="destinationPath">The path to the destination file.</param>
      public Task MoveFileAsync(string sourcePath, string destinationPath)
      {
         return TaskEx.Run(() => MoveFile(sourcePath, destinationPath));
      }

      /// <summary>
      /// Determines whether a file exists.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      public bool FileExists(string path)
      {
         var combined = Path.Append(root, path);
         return area.FileExists(combined);
      }

      /// <summary>
      /// Determines whether a file exists.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      public Task<bool> FileExistsAsync(string path)
      {
         var exists = FileExists(path);
         return TaskEx.FromResult(exists);
      }

      /// <summary>
      /// Deletes a file.
      /// If the file does not exist, this does nothing.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      public bool DeleteFile(string path)
      {
         var combined = Path.Append(root, path);
         bool fileExists;
         try
         {
            // Unlike the .NET file system, this throws an exception when the file does not exist.
            // And worse, the exception means either the file is open or it doesn't exist.
            // So we optimistically try to delete the file (no sense in checking if the file
            // exists first because (1) it's slower and in most cases it probably will exist, 
            // and (2) multithreading could result in file getting delete right after we 
            // checked if it exists so there's really no point.  Anyway, we try to delete
            // it and sort it out in the exception handler.
            // scenario so 
            area.DeleteFile(combined);
            fileExists = true;
         }
         catch (IsolatedStorageException)
         {
            // Check if the file exists
            try
            {
               fileExists = area.FileExists(combined);
            }
            catch (Exception)
            {
               // If this failed then we say the file existed so the the original exception will be rethrown.
               fileExists = true;
            }
            // If the file existed then rethrow the original exception.
            // If the file did not exist, then suppress the original exception
            // and assume it must have been a result of the file not existing.
            if (fileExists)
               throw;
         }
         return fileExists;
      }

      /// <summary>
      /// Deletes a file.
      /// If the file does not exist, this does nothing.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <returns>True if the file was deleted; false if the file did not exist.</returns>
      public Task<bool> DeleteFileAsync(string path)
      {
         var existed = DeleteFile(path);
         return TaskEx.FromResult(existed);
      }

      /// <summary>
      /// Writes text to a file.
      /// If the file does not exist then it's created; if it does exist then it's overwritten.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="text">The contents of the file.</param>
      public void WriteAllText(string path, string text)
      {
         using (var stream = OpenForWrite(path))
         using (var writer = new StreamWriter(stream))
            writer.Write(text);
      }

      /// <summary>
      /// Writes text to a file.
      /// If the file does not exist then it's created; if it does exist then it's overwritten.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="text">The contents of the file.</param>
      public async Task WriteAllTextAsync(string path, string text)
      {
         using (var stream = OpenForWrite(path))
         using (var writer = new StreamWriter(stream))
            await writer.WriteAsync(text);
      }

      /// <summary>
      /// Reads the contexts of a file as text.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <returns>The contents of the file.</returns>
      public string ReadAllText(string path)
      {
         using (var stream = OpenForRead(path))
         using (var reader = new StreamReader(stream))
            return reader.ReadToEnd();
      }

      /// <summary>
      /// Reads the contexts of a file as text.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <returns>The contents of the file.</returns>
      public async Task<string> ReadAllTextAsync(string path)
      {
         using (var stream = OpenForRead(path))
         using (var reader = new StreamReader(stream))
            return await reader.ReadToEndAsync();
      }

      /// <summary>
      /// Opens an existing file for reading.
      /// The sharing mode is read-only.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="mode">Open mode.</param>
      /// <param name="share">Share mode.</param>
      /// <returns>Stream that can be read from but not written to; must be disposed to close file.</returns>
      public Stream OpenForRead(string path, OpenMode mode = OpenMode.Open, OpenShare share = OpenShare.Read)
      {
         var combined = Path.Append(root, path);
         return area.OpenFile(combined, ToMode(mode), FileAccess.Read, ToShare(share));
      }

      /// <summary>
      /// Opens an existing file for reading.
      /// The sharing mode is read-only.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="mode">Open mode.</param>
      /// <param name="share">Share mode.</param>
      /// <returns>Stream that can be read from but not written to; must be disposed to close file.</returns>
      public Task<Stream> OpenForReadAsync(string path, OpenMode mode = OpenMode.Open, OpenShare share = OpenShare.Read)
      {
         var stream = OpenForRead(path, mode, share);
         return TaskEx.FromResult(stream);
      }

      /// <summary>
      /// Opens a file for writing; if the file does not exist, it is created.
      /// The sharing mode is none.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="mode">Open mode.</param>
      /// <param name="share">Share mode.</param>
      /// <returns>Stream that can be read from and written to; must be disposed to close file.</returns>
      public Stream OpenForWrite(string path, OpenMode mode = OpenMode.OpenOrCreate, OpenShare share = OpenShare.None)
      {
         var combined = Path.Append(root, path);
         // Read/write access matches what win8 provides
         return area.OpenFile(combined, ToMode(mode), FileAccess.ReadWrite, ToShare(share));
      }

      /// <summary>
      /// Opens a file for writing; if the file does not exist, it is created.
      /// The sharing mode is none.
      /// </summary>
      /// <param name="path">The path to the file.</param>
      /// <param name="mode">Open mode.</param>
      /// <param name="share">Share mode.</param>
      /// <returns>Stream that can be read from and written to; must be disposed to close file.</returns>
      public Task<Stream> OpenForWriteAsync(string path, OpenMode mode = OpenMode.OpenOrCreate, OpenShare share = OpenShare.None)
      {
         var stream = OpenForWrite(path, mode, share);
         return TaskEx.FromResult(stream);
      }

      static FileMode ToMode(OpenMode mode)
      {
         switch (mode)
         {
            case OpenMode.CreateNew:
               return FileMode.CreateNew;
            case OpenMode.Create:
               return FileMode.Create;
            case OpenMode.Open:
               return FileMode.Open;
            case OpenMode.OpenOrCreate:
               return FileMode.OpenOrCreate;
            //case OpenMode.Truncate:
            //   return FileMode.Truncate;
            //case OpenMode.Append:
            //   return FileMode.Append;
            default:
               throw new ArgumentOutOfRangeException("mode");
         }
      }

      //static FileAccess ToAccess(OpenAccess access)
      //{
      //   switch (access)
      //   {
      //      case OpenAccess.Read:
      //         return FileAccess.Read;
      //      case OpenAccess.Write:
      //         return FileAccess.Write;
      //      case OpenAccess.ReadWrite:
      //         return FileAccess.ReadWrite;
      //      default:
      //         throw new ArgumentOutOfRangeException("access");
      //   }
      //}

      static FileShare ToShare(OpenShare share)
      {
         switch (share)
         {
            case OpenShare.None:
               return FileShare.None;
            case OpenShare.Read:
               return FileShare.Read;
            case OpenShare.Write:
               return FileShare.Write;
            case OpenShare.ReadWrite:
               return FileShare.ReadWrite;
            case OpenShare.Delete:
               return FileShare.Delete;
            default:
               throw new ArgumentOutOfRangeException("share");
         }
      }
   }
}